package com.easipass.framework.eirframeworkcoredsl;

import java.util.*;

public class ServiceParser {

    public final static ArgumentNode[] NO_ARGUMENTS = new ArgumentNode[0];

    private final Tokens tokens;

    protected String appName;

    public ServiceParser(Tokens tokens, String appName) {
        this.tokens = tokens;
        this.appName = appName;
    }

    public Tokens getTokens() {
        return tokens;
    }

    protected ServiceNode eatService(boolean transitionsAllowed) {
        Token name = tokens.next();
        if (!name.isKind(TokenKind.IDENTIFIER)) {
            tokens.raiseException(name.startPos, DSLMessage.EXPECTED_APPNAME,
                    name.data != null ? name.data : new String(name.getKind().tokenChars));
        }
        getTokens().checkpoint();

        ArgumentNode[] arguments =  eatAppArgs();
        if (peek(TokenKind.DOUBLE_MINUS)) {
            getTokens().raiseException(peek().startPos, DSLMessage.SERVICE_ARGUMENTS_NOT_ALLOWED_UNLESS_IN_APP_MODE);
        }
        List<TransitionNode> transitions = transitionsAllowed ? maybeEatTransitions() : Collections.emptyList();
        return new ServiceNode(name,appName, arguments, transitions);
    }

    private List<TransitionNode> maybeEatTransitions() {
        List<TransitionNode> transitions = new ArrayList<>();
        Token transitionOn = null;
        while (true) {
            if (peek(TokenKind.ARROW)) {
                getTokens().raiseException(peek().startPos, DSLMessage.SERVICE_ARROW_SHOULD_BE_PRECEDED_BY_CODE);
                break;
            }
            Token possibleArrow = peek(+1);
            if (possibleArrow == null || possibleArrow.getKind() != TokenKind.ARROW) {
                break;
            }
            transitionOn = peek();
            if (transitionOn == null || (transitionOn.getKind() != TokenKind.IDENTIFIER
                    && transitionOn.getKind() != TokenKind.LITERAL_STRING && transitionOn.getKind() != TokenKind.STAR)) {
                break;
            }
            eat();
            if (!maybeEat(TokenKind.ARROW)) {
                getTokens().raiseException(transitionOn.startPos, DSLMessage.SERVICE_MISSING_TRANSITION_ARROW);
            }
            TransitionNode t = null;
            if (maybeEat(TokenKind.COLON)) {
                Token labelReference = eat(TokenKind.IDENTIFIER);
                t = TransitionNode.toLabelReference(transitionOn, labelReference);
            } else {
                Token label = maybeEatLabel();
                ServiceNode serviceNode = eatService(false);
                serviceNode.setLabel(label);
                t = TransitionNode.toAnotherService(transitionOn, serviceNode);
            }
            if (t.isExitCodeCheck() && !t.getStatusToCheck().equals("*")) {
                try {
                    Integer.parseInt(t.getStatusToCheck());
                } catch (NumberFormatException nfe) {
                    getTokens().raiseException(transitionOn.startPos,
                            DSLMessage.SERVICE_UNQUOTED_TRANSITION_CHECK_MUST_BE_NUMBER, t.getStatusToCheck());
                }
            }
            transitions.add(t);
        }
        return transitions;
    }

    protected ArgumentNode[] eatAppArgs() {
        List<ArgumentNode> args = null;
        if (tokens.peek(TokenKind.DOUBLE_MINUS) && tokens.isNextAdjacent()) {
            tokens.raiseException(tokens.peek().startPos, DSLMessage.EXPECTED_WHITESPACE_AFTER_APP_BEFORE_ARGUMENT);
        }
        while (tokens.peek(TokenKind.DOUBLE_MINUS)) {
            Token dashDash = tokens.next();
            if (tokens.peek(TokenKind.IDENTIFIER) && !tokens.isNextAdjacent()) {
                tokens.raiseException(tokens.peek().startPos, DSLMessage.NO_WHITESPACE_BEFORE_ARG_NAME);
            }
            List<Token> argNameComponents = eatDottedName();
            if (tokens.peek(TokenKind.EQUALS) && !tokens.isNextAdjacent()) {
                tokens.raiseException(tokens.peek().startPos, DSLMessage.NO_WHITESPACE_BEFORE_ARG_EQUALS);
            }
            tokens.eat(TokenKind.EQUALS);
            if (tokens.peek(TokenKind.IDENTIFIER) && !tokens.isNextAdjacent()) {
                tokens.raiseException(tokens.peek().startPos, DSLMessage.NO_WHITESPACE_BEFORE_ARG_VALUE);
            }
            Token t = tokens.peek();
            String argValue = eatArgValue();
            tokens.checkpoint();
            if (args == null) {
                args = new ArrayList<ArgumentNode>();
            }
            args.add(new ArgumentNode(toData(argNameComponents), argValue, dashDash.startPos, t.endPos));
        }
        return args == null ? NO_ARGUMENTS : args.toArray(new ArgumentNode[args.size()]);
    }

    protected String eatArgValue() {
        Token t = tokens.next();
        String argValue = null;
        if (t.getKind() == TokenKind.IDENTIFIER) {
            argValue = t.data;
        } else if (t.getKind() == TokenKind.LITERAL_STRING) {
            String quotesUsed = t.data.substring(0, 1);
            argValue = t.data.substring(1, t.data.length() - 1).replace(quotesUsed + quotesUsed, quotesUsed);
        } else {
            tokens.raiseException(t.startPos, DSLMessage.EXPECTED_ARGUMENT_VALUE, t.data);
        }
        return argValue;
    }

    protected List<Token> eatDottedName() {
        List<Token> result = new ArrayList<Token>(3);
        Token name = tokens.next();
        if (!name.isKind(TokenKind.IDENTIFIER)) {
            tokens.raiseException(name.startPos, DSLMessage.NOT_EXPECTED_TOKEN,
                    name.data != null ? name.data : new String(name.getKind().tokenChars));
        }
        result.add(name);
        while (tokens.peek(TokenKind.DOT)) {
            if (!tokens.isNextAdjacent()) {
                tokens.raiseException(tokens.peek().startPos, DSLMessage.NO_WHITESPACE_IN_DOTTED_NAME);
            }
            result.add(tokens.next()); // consume dot
            if (tokens.peek(TokenKind.IDENTIFIER) && !tokens.isNextAdjacent()) {
                tokens.raiseException(tokens.peek().startPos, DSLMessage.NO_WHITESPACE_IN_DOTTED_NAME);
            }
            result.add(tokens.eat(TokenKind.IDENTIFIER));
        }
        return result;
    }

    protected LabelNode toLabelNode(Token label) {
        return label == null ? null : new LabelNode(label.data, label.startPos, label.endPos);
    }

    protected String toData(Iterable<Token> iterable) {
        StringBuilder result = new StringBuilder();
        for (Token t : iterable) {
            if (t.getKind().hasPayload()) {
                result.append(t.data);
            } else {
                result.append(t.getKind().tokenChars);
            }
        }
        return result.toString();
    }

    protected List<String> tokenListToStringList(List<Token> tokens) {
        if (tokens.isEmpty()) {
            return Collections.<String>emptyList();
        }
        List<String> data = new ArrayList<String>();
        for (Token token : tokens) {
            data.add(token.data);
        }
        return data;
    }

    protected String toString(Token t) {
        return t.getKind().hasPayload() ? t.stringValue() : new String(t.kind.getTokenChars());
    }

    protected boolean isValidName(String name) {
        if (name.length() == 0 || !Character.isJavaIdentifierStart(name.charAt(0))) {
            return false;
        }

        for (int i = 1, max = name.length(); i < max; i++) {
            char ch = name.charAt(i);
            if (!(Character.isJavaIdentifierPart(ch))) {
                return false;
            }
        }
        return true;
    }

    private Token eat(TokenKind tokenKind) {
        return getTokens().eat(tokenKind);
    }

    private Token eat() {
        return getTokens().eat();
    }

    private boolean peek(TokenKind desiredTokenKind) {
        return getTokens().peek(desiredTokenKind);
    }

    private Token peek(int howFarAhead) {
        return getTokens().peek(howFarAhead);
    }

    private Token peek() {
        return getTokens().peek();
    }

    protected boolean maybeEat(TokenKind desiredTokenKind) {
        if (peek(desiredTokenKind)) {
            eat(desiredTokenKind);
            return true;
        } else {
            return false;
        }
    }

    protected Token maybeEatLabel() {
        if (peek(TokenKind.IDENTIFIER) && (peek(1) != null && peek(1).isKind(TokenKind.COLON))) {
            Token labelToken = eat();
            eat(TokenKind.COLON);
            return labelToken;
        }
        return null;
    }

    protected LabelledServiceNode parseServiceNode() {
        if (maybeEat(TokenKind.OPEN_PAREN)) {
            LabelledServiceNode node = parseServiceNode();
            getTokens().eat(TokenKind.CLOSE_PAREN);
            return node;
        }
        Token label = maybeEatLabel();
        if (label != null) {
            Token secondLabel;
            if (getTokens().peek(TokenKind.OPEN_PAREN)) {
                getTokens().raiseException(getTokens().peek().startPos, DSLMessage.SERVICE_NO_LABELS_ON_PARENS);
            } else if ((secondLabel = maybeEatLabel()) != null) {
                getTokens().raiseException(secondLabel.startPos, DSLMessage.SERVICE_NO_DOUBLE_LABELS);
            }
        }
        if (getTokens().peek(TokenKind.LT)) {
            LabelledServiceNode node = parseSplit();
            node.setLabel(label);
            return parseFlow(node);
        }

        ServiceNode serviceNode = eatService(true);
        serviceNode.setLabel(label);
        return parseFlow(serviceNode);
    }

    protected LabelledServiceNode parseSplit() {
        List<LabelledServiceNode> flows = new ArrayList<>();
        Token startSplit = getTokens().eat(TokenKind.LT);
        flows.add(parseServiceNode());
        while (maybeEat(TokenKind.DOUBLEPIPE)) {
            flows.add(parseServiceNode());
        }
        Token endSplit = getTokens().eat(TokenKind.GT);
        return new SplitNode(startSplit.startPos, endSplit.endPos, flows);
    }

    protected FlowNode parseFlow(LabelledServiceNode firstNodeInFlow) {
        List<LabelledServiceNode> nodes = new ArrayList<>();
        nodes.add(firstNodeInFlow);
        while (maybeEat(TokenKind.ANDAND) ) {
            LabelledServiceNode nextNode = parseServiceNode();
            if (nextNode instanceof FlowNode) {
                nodes.addAll(nextNode.getSeries());
            } else {
                nodes.add(nextNode);
            }
        }
        FlowNode f = new FlowNode(nodes);
        f.setLabel(firstNodeInFlow.getLabel());
        return f;
    }

    protected boolean nextTokenIsOnNewline() {
        Token nextToken = getTokens().peek();
        Token lastToken = getTokens().peek(-1);
        int lastTokenLine = getTokens().getLine(lastToken);
        int nextTokenLine = getTokens().getLine(nextToken);
        return nextTokenLine > lastTokenLine;
    }
}
